package org.go.xml;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;

import org.go.Go;
import org.go.domain.GoDomain;
import org.go.domain.TriggerType;
import org.go.expcetion.JobPersistenceException;
import org.go.expcetion.ObjectAlreadyExistsException;
import org.go.expcetion.SchedulerException;
import org.go.expcetion.ValidationException;
import org.go.log.Log;
import org.go.log.LoggerFactory;
import org.go.scheduler.AlwaysScheduleBuilder;
import org.go.scheduler.ScheduleBuilder;
import org.go.scheduler.Scheduler;
import org.go.spi.ClassLoadHelper;
import org.go.trigger.RepeatScheduleBuilder;
import org.go.trigger.Trigger;
import org.go.trigger.TriggerBuilder;
import org.go.util.XmlUtil;
import org.go.work.Work;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * 
 * @author hejie
 *
 */
public class XMLSchedulingDataProcessor {
	private static final Log log = LoggerFactory.getLog(XMLSchedulingDataProcessor.class);

	public static RepeatScheduleBuilder repeatSchedule() {
		return new RepeatScheduleBuilder();
	}

	private ClassLoadHelper classLoadHelper = null;
	protected List<String> jobGroupsToNeverDelete = new LinkedList<String>();
	protected List<Work> loadedJobs = new LinkedList<Work>();
	protected List<Trigger> loadedTriggers = new LinkedList<Trigger>();
	protected Map<String, Work> loadedWorkers = new LinkedHashMap<String, Work>();
	protected List<String> triggerGroupsToNeverDelete = new LinkedList<String>();

	protected Collection validationExceptions = new ArrayList();

	public XMLSchedulingDataProcessor(ClassLoadHelper classLoadHelper) {
		this.setClassLoadHelper(classLoadHelper);
	}

	protected void addJobToSchedule(Work worker) {
		loadedWorkers.put(worker.getWorkId(), worker);
	}

	protected void addTriggerToSchedule(Trigger trigger) {
		loadedTriggers.add(trigger);
	}

	protected InputStream getInputStream(String fileName) {
		return this.classLoadHelper.getResourceAsStream(fileName);
	}

	protected void maybeThrowValidationException() throws ValidationException {
		if (validationExceptions.size() > 0) {
			throw new ValidationException("Encountered " + validationExceptions.size() + " validation exceptions.", validationExceptions);
		}
	}

	protected void prepForProcessing() {
		//		clearValidationExceptions();
		//
		//		setOverWriteExistingData(true);
		//		setIgnoreDuplicates(false);
		//
		//		jobGroupsToDelete.clear();
		//		jobsToDelete.clear();
		//		triggerGroupsToDelete.clear();
		//		triggersToDelete.clear();
		//
		//		loadedJobs.clear();
		//		loadedTriggers.clear();
	}

	/**
	 * 
	 * @param is
	 * @throws SAXException
	 * @throws IOException
	 * @throws XPathException
	 * @throws ClassNotFoundException
	 * @throws ParseException 
	 */
	protected void process(InputSource is) throws SAXException, IOException, XPathException, ClassNotFoundException, ParseException, Exception {
		try {
			// load the document
			Document document = XmlUtil.readXML(is.getByteStream());
			XPath xpath = XmlUtil.buildXpath();
			// Extract pre-processing commands
			NodeList deleteJobGroupNodes = (NodeList) xpath.evaluate("/job-scheduling-data/pre-processing-commands/delete-jobs-in-group", document, XPathConstants.NODESET);
			//
			log.debug("Found " + deleteJobGroupNodes.getLength() + " delete job group commands.");
			//		for (int i = 0; i < deleteJobGroupNodes.getLength(); i++) {
			//			Node node = deleteJobGroupNodes.item(i);
			//			String t = node.getTextContent();
			//			if (t == null || (t = t.trim()).length() == 0)
			//				continue;
			//			jobGroupsToDelete.add(t);
			//		}
			NodeList deleteTriggerGroupNodes = (NodeList) xpath.evaluate("/job-scheduling-data/pre-processing-commands/delete-triggers-in-group", document, XPathConstants.NODESET);
			//
			log.debug("Found " + deleteTriggerGroupNodes.getLength() + " delete trigger group commands.");
			//
			//		for (int i = 0; i < deleteTriggerGroupNodes.getLength(); i++) {
			//			Node node = deleteTriggerGroupNodes.item(i);
			//			String t = node.getTextContent();
			//			if (t == null || (t = t.trim()).length() == 0)
			//				continue;
			//			triggerGroupsToDelete.add(t);
			//		}
			NodeList deleteJobNodes = (NodeList) xpath.evaluate("/job-scheduling-data/pre-processing-commands/delete-job", document, XPathConstants.NODESET);
			//
			log.debug("Found {} delete job commands.", deleteJobNodes.getLength());
			//
			//		for (int i = 0; i < deleteJobNodes.getLength(); i++) {
			//			Node node = deleteJobNodes.item(i);
			//			String name = getTrimmedToNullString(xpath, "q:name", node);
			//			String group = getTrimmedToNullString(xpath, "q:group", node);
			//			if (name == null)
			//				throw new ParseException("Encountered a 'delete-job' command without a name specified.", -1);
			//			jobsToDelete.add(new JobKey(name, group));
			//		}
			NodeList deleteTriggerNodes = (NodeList) xpath.evaluate("/job-scheduling-data/pre-processing-commands/delete-trigger", document, XPathConstants.NODESET);
			//
			log.debug("Found {} delete trigger commands.", deleteTriggerNodes.getLength());
			//
			//		for (int i = 0; i < deleteTriggerNodes.getLength(); i++) {
			//			Node node = deleteTriggerNodes.item(i);
			//
			//			String name = getTrimmedToNullString(xpath, "q:name", node);
			//			String group = getTrimmedToNullString(xpath, "q:group", node);
			//
			//			if (name == null)
			//				throw new ParseException("Encountered a 'delete-trigger' command without a name specified.", -1);
			//			triggersToDelete.add(new TriggerKey(name, group));
			//		}
			//
			// Extract directives
			Boolean overWrite = XmlUtil.getBoolean(xpath, "/job-scheduling-data/processing-directives/overwrite-existing-data", document);
			//		if (overWrite == null) {
			//			log.debug("Directive 'overwrite-existing-data' not specified, defaulting to " + isOverWriteExistingData());
			//		} else {
			//			log.debug("Directive 'overwrite-existing-data' specified as: " + overWrite);
			//			setOverWriteExistingData(overWrite);
			//		}
			Boolean ignoreDupes = XmlUtil.getBoolean(xpath, "/job-scheduling-data/processing-directives/ignore-duplicates", document);
			//		if (ignoreDupes == null) {
			//			log.debug("Directive 'ignore-duplicates' not specified, defaulting to " + isIgnoreDuplicates());
			//		} else {
			//			log.debug("Directive 'ignore-duplicates' specified as: " + ignoreDupes);
			//			setIgnoreDuplicates(ignoreDupes);
			//		}
			// Extract Worker definitions...
			//job-scheduling-data
			NodeList jobNodes = (NodeList) xpath.evaluate("/job-scheduling-data/schedule/worker", document, XPathConstants.NODESET);
			//
			log.debug("Found {} worker definitions.", jobNodes.getLength());
			//scan workers
			for (int i = 0; i < jobNodes.getLength(); i++) {
				Node jobDetailNode = jobNodes.item(i);
				//
				String workerId = XmlUtil.getTrimmedToNullString(xpath, "id", jobDetailNode);
				String workerName = XmlUtil.getTrimmedToNullString(xpath, "name", jobDetailNode);
				String jobClassName = XmlUtil.getTrimmedToNullString(xpath, "job-class", jobDetailNode);
				//String workerDesc = getTrimmedToNullString(xpath, "q:description", jobDetailNode);
				//int threadcount = getTrimmedToInt(xpath, "q:threadcount", jobDetailNode);
				//String triggerId = getTrimmedToNullString(xpath, "q:triggerid", jobDetailNode);
				//String poolId = getTrimmedToNullString(xpath, "q:poolid", jobDetailNode);
				//t = getTrimmedToNullString(xpath, "q:durability", jobDetailNode);
				//boolean jobDurability = (t != null) && t.equals("true");
				//t = getTrimmedToNullString(xpath, "q:recover", jobDetailNode);
				//boolean jobRecoveryRequested = (t != null) && t.equals("true");
				Class<Go> jobClass = (Class<Go>) classLoadHelper.loadClass(jobClassName);
				Go go = jobClass.newInstance();
				Work worker = null;
				//new Work(go);

				worker.setWorkId(workerName);
				//worker.setWorkerDesc(workerDesc);
				worker.setWorkId(workerId);
				worker.setGo(go);
				log.debug("Parsed job definition: {}", worker);
				addJobToSchedule(worker);
				//Worker jobDetail = newJob(jobClass).withIdentity(jobName, jobGroup).withDescription(jobDescription).storeDurably(jobDurability).requestRecovery(jobRecoveryRequested).build();
				//
				//			NodeList jobDataEntries = (NodeList) xpath.evaluate("q:job-data-map/q:entry", jobDetailNode, XPathConstants.NODESET);
				//
				//			for (int k = 0; k < jobDataEntries.getLength(); k++) {
				//				Node entryNode = jobDataEntries.item(k);
				//				String key = getTrimmedToNullString(xpath, "q:key", entryNode);
				//				String value = getTrimmedToNullString(xpath, "q:value", entryNode);
				//				jobDetail.getJobDataMap().put(key, value);
				//			}
			}
			// Extract Trigger definitions...
			NodeList triggerEntries = (NodeList) xpath.evaluate("/job-scheduling-data/schedule/trigger", document, XPathConstants.NODESET);
			log.debug("Found {} trigger definitions.", triggerEntries.getLength());
			//
			for (int j = 0; j < triggerEntries.getLength(); j++) {
				ScheduleBuilder sched = null;
				Node triggerNode = triggerEntries.item(j);
				String triggerType = "";
				String triggerName = XmlUtil.getTrimmedToNullString(xpath, "name", triggerNode);
				String triggerId = XmlUtil.getTrimmedToNullString(xpath, "id", triggerNode);
				String triggerDescription = XmlUtil.getTrimmedToNullString(xpath, "description", triggerNode);
				String triggerMisfireInstructionConst = XmlUtil.getTrimmedToNullString(xpath, "misfire-instruction", triggerNode);
				String triggerPriorityString = XmlUtil.getTrimmedToNullString(xpath, "priority", triggerNode);
				String triggerCalendarRef = XmlUtil.getTrimmedToNullString(xpath, "calendar-name", triggerNode);
				//String triggerJobName = getTrimmedToNullString(xpath, "q:job-name", triggerNode);
				//String triggerJobGroup = getTrimmedToNullString(xpath, "q:job-group", triggerNode);
				int triggerPriority = Trigger.DEFAULT_PRIORITY;
				if (triggerPriorityString != null) {
					triggerPriority = Integer.valueOf(triggerPriorityString);
				}
				//
				String startTimeString = XmlUtil.getTrimmedToNullString(xpath, "start-time", triggerNode);
				String startTimeFutureSecsString = XmlUtil.getTrimmedToNullString(xpath, "start-time-seconds-in-future", triggerNode);
				String endTimeString = XmlUtil.getTrimmedToNullString(xpath, "end-time", triggerNode);
				//
				Date triggerStartTime = null;
				if (startTimeFutureSecsString != null) {
					triggerStartTime = new Date(System.currentTimeMillis() + (Long.valueOf(startTimeFutureSecsString) * 1000L));
				} else {
					triggerStartTime = (startTimeString == null || startTimeString.length() == 0 ? new Date() : GoDomain.dateFormat.parse(startTimeString));
				}
				Date triggerEndTime = endTimeString == null || endTimeString.length() == 0 ? null : GoDomain.dateFormat.parse(endTimeString);
				//			TriggerKey triggerKey = triggerKey(triggerName, triggerGroup);
				//			ScheduleBuilder sched = null;
				Node subNode1 = XmlUtil.getTrimmedToNullObject(xpath, "always", triggerNode);
				Node subNode2 = XmlUtil.getTrimmedToNullObject(xpath, "repeat", triggerNode);
				Node subNode3 = XmlUtil.getTrimmedToNullObject(xpath, "cron", triggerNode);
				if (subNode1 != null && subNode2 != null && subNode3 != null) {
					throw new Exception();
				} else if (subNode1 != null && subNode2 != null) {
					throw new Exception();
				} else if (subNode1 != null && subNode3 != null) {
					throw new Exception();
				} else if (subNode2 != null && subNode3 != null) {
					throw new Exception();
				} else if (subNode1 == null && subNode2 == null && subNode3 == null) {
					throw new Exception();
				} else if (subNode1 != null && subNode1.getNodeName().equals("always")) {
					sched = new AlwaysScheduleBuilder();
					triggerType = TriggerType.ALWAYS.getName();
				} else if (subNode2 != null && subNode2.getNodeName().equals("repeat")) {
					triggerType = TriggerType.REPEAT.getName();
					String repeatCountString = XmlUtil.getTrimmedToNullString(xpath, "repeat-count", triggerNode);
					String repeatIntervalString = XmlUtil.getTrimmedToNullString(xpath, "repeat-interval", triggerNode);
					int repeatCount = repeatCountString == null ? GoDomain.REPEAT_INDEFINITELY : Integer.parseInt(repeatCountString);
					long repeatInterval = repeatIntervalString == null ? 0 : Long.parseLong(repeatIntervalString);
					sched = repeatSchedule().withIntervalInMilliseconds(repeatInterval).withRepeatCount(repeatCount);
					//				if (triggerMisfireInstructionConst != null && triggerMisfireInstructionConst.length() != 0) {
					//					if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_FIRE_NOW"))
					//						((SimpleScheduleBuilder) sched).withMisfireHandlingInstructionFireNow();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT"))
					//						((SimpleScheduleBuilder) sched).withMisfireHandlingInstructionNextWithExistingCount();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT"))
					//						((SimpleScheduleBuilder) sched).withMisfireHandlingInstructionNextWithRemainingCount();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT"))
					//						((SimpleScheduleBuilder) sched).withMisfireHandlingInstructionNowWithExistingCount();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT"))
					//						((SimpleScheduleBuilder) sched).withMisfireHandlingInstructionNowWithRemainingCount();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_SMART_POLICY")) {
					//						// do nothing.... (smart policy is default)
					//					} else
					//						throw new ParseException("Unexpected/Unhandlable Misfire Instruction encountered '" + triggerMisfireInstructionConst + "', for trigger: " + triggerKey, -1);
					//}
				} else if (subNode3 != null && subNode3.getNodeName().equals("cron")) {
					triggerType = TriggerType.CONDITION.getName();
					String cronExpression = XmlUtil.getTrimmedToNullString(xpath, "cron-expression", triggerNode);
					String timezoneString = XmlUtil.getTrimmedToNullString(xpath, "time-zone", triggerNode);
					TimeZone tz = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString);
					//				sched = cronSchedule(cronExpression).inTimeZone(tz);
					//
					//				if (triggerMisfireInstructionConst != null && triggerMisfireInstructionConst.length() != 0) {
					//					if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_DO_NOTHING"))
					//						((CronScheduleBuilder) sched).withMisfireHandlingInstructionDoNothing();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_FIRE_ONCE_NOW"))
					//						((CronScheduleBuilder) sched).withMisfireHandlingInstructionFireAndProceed();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_SMART_POLICY")) {
					// do nothing.... (smart policy is default)
					//					} else
					//						throw new ParseException("Unexpected/Unhandlable Misfire Instruction encountered '" + triggerMisfireInstructionConst + "', for trigger: " + triggerKey, -1);
					//				}
				} else if (triggerNode.getNodeName().equals("calendar-interval")) {
					String repeatIntervalString = XmlUtil.getTrimmedToNullString(xpath, "repeat-interval", triggerNode);
					String repeatUnitString = XmlUtil.getTrimmedToNullString(xpath, "repeat-interval-unit", triggerNode);
					int repeatInterval = Integer.parseInt(repeatIntervalString);
					//IntervalUnit repeatUnit = IntervalUnit.valueOf(repeatUnitString);
					//				sched = calendarIntervalSchedule().withInterval(repeatInterval, repeatUnit);
					//				if (triggerMisfireInstructionConst != null && triggerMisfireInstructionConst.length() != 0) {
					//					if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_DO_NOTHING"))
					//						((CalendarIntervalScheduleBuilder) sched).withMisfireHandlingInstructionDoNothing();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_FIRE_ONCE_NOW"))
					//						((CalendarIntervalScheduleBuilder) sched).withMisfireHandlingInstructionFireAndProceed();
					//					else if (triggerMisfireInstructionConst.equals("MISFIRE_INSTRUCTION_SMART_POLICY")) {
					//						// do nothing.... (smart policy is default)
					//					} else
					//						throw new ParseException("Unexpected/Unhandlable Misfire Instruction encountered '" + triggerMisfireInstructionConst + "', for trigger: " + triggerKey, -1);
					//				}
				} else {
					throw new ParseException("Unknown trigger type: " + triggerNode.getNodeName(), -1);
				}
				Trigger trigger = TriggerBuilder.newTrigger(triggerType).withIdentity(triggerId, triggerName).withDescription(triggerDescription).startAt(triggerStartTime).endAt(triggerEndTime).withPriority(triggerPriority)
						.modifiedByCalendar(triggerCalendarRef).withSchedule(sched).build();
				NodeList jobDataEntries = (NodeList) xpath.evaluate("q:job-data-map/q:entry", triggerNode, XPathConstants.NODESET);
				//
				//			for (int k = 0; k < jobDataEntries.getLength(); k++) {
				//				Node entryNode = jobDataEntries.item(k);
				//				String key = getTrimmedToNullString(xpath, "q:key", entryNode);
				//				String value = getTrimmedToNullString(xpath, "q:value", entryNode);
				//				trigger.getJobDataMap().put(key, value);
				//			}
				log.debug("Parsed trigger definition: {}", trigger);
				addTriggerToSchedule(trigger);
			}
			// Extract WorkerDesc definitions...
			NodeList workerDescEntries = (NodeList) xpath.evaluate("/job-scheduling-data/schedule/workerdesc", document, XPathConstants.NODESET);
			for (int m = 0; m < workerDescEntries.getLength(); m++) {
				Node workerDescNode = workerDescEntries.item(m);
				String workerDescId = XmlUtil.getTrimmedToNullString(xpath, "workerdescid", workerDescNode);
				//String workerDescName = getTrimmedToNullString(xpath, "q:workerdescname", workerDescNode);
				String description = XmlUtil.getTrimmedToNullString(xpath, "description", workerDescNode);
				String workerId = XmlUtil.getTrimmedToNullString(xpath, "workerid", workerDescNode);
				//String poolId = getTrimmedToNullString(xpath, "q:poolid", workerDescNode);
				int threadcount = XmlUtil.getTrimmedToInt(xpath, "threadcount", workerDescNode, 1);
				String triggerId = XmlUtil.getTrimmedToNullString(xpath, "triggerid", workerDescNode);
				Work workerDesc = null;
				//new Work(null);
				//workerDesc.setId(workerDescId);
				//	workerDesc.setPoolId(poolId);
				//workerDesc.setThreadcount(threadcount);
				//workerDesc.setTriggerId(triggerId);
				//workerDesc.setDescription(description);
				//workerDesc.setWorker(getWorkerById(workerId));
				loadedJobs.add(workerDesc);
			}
		} catch (InstantiationException e) {
			log.error("{}", e);
		} catch (IllegalAccessException e) {
			log.error("{}", e);
		}
	}

	protected void processFile(String fileName, String systemId) throws ValidationException, SAXException, IOException, ClassNotFoundException, ParseException, XPathException, Exception {
		prepForProcessing();
		log.info("Parsing XML file: {} with systemId: {}", systemId, fileName);
		InputSource is = new InputSource(getInputStream(fileName));
		is.setSystemId(systemId);
		process(is);
		maybeThrowValidationException();
	}

	protected Object resolveSchemaSource() {
		InputSource inputSource = null;

		InputStream is = null;

		//URL url = null;

		try {
			is = classLoadHelper.getResourceAsStream(GoDomain.GO_XSD_PATH_IN_JAR);
		} finally {
			if (is != null) {
				inputSource = new InputSource(is);
				inputSource.setSystemId(GoDomain.QUARTZ_SCHEMA_WEB_URL);
				log.debug("Utilizing schema packaged in local go distribution jar.");
			} else {
				log.info("Unable to load local schema packaged in go distribution jar. Utilizing schema online at {}", GoDomain.QUARTZ_SCHEMA_WEB_URL);
				return GoDomain.QUARTZ_SCHEMA_WEB_URL;
			}
		}
		return inputSource;
	}

	void executePreProcessCommands(Scheduler sched) {

	}

	List<Trigger> getLoadedTriggers() {
		return loadedTriggers;
	}

	Work getWorkerById(String workerId) {
		Work worker1 = loadedWorkers.get(workerId);
		Work worker = null;
		//new Work(worker1.getGo());
		worker.setWorkId(worker1.getWorkId());
		//worker.setWorkerDesc(worker1.getWorkerDesc());
		worker.setWorkId(worker1.getWorkId());
		return worker;
	}

	public void addJobGroupToNeverDelete(String group) {
		if (group != null) {
			jobGroupsToNeverDelete.add(group);
		}
	}

	public void addTriggerGroupToNeverDelete(String group) {
		if (group != null)
			triggerGroupsToNeverDelete.add(group);
	}

	public ClassLoadHelper getClassLoadHelper() {
		return classLoadHelper;
	}

	public List<Work> getLoadedJobs() {
		return loadedJobs;
	}

	public void processFileAndScheduleJobs(String fileName, String systemId, Scheduler sched) throws Exception, JobPersistenceException {
		processFile(fileName, systemId);
		executePreProcessCommands(sched);
		scheduleJobs(sched);
	}

	public void scheduleJobs(Scheduler sched) throws JobPersistenceException, SchedulerException {
		List<Trigger> triggers = new LinkedList(getLoadedTriggers());
		List<Work> jobs = new LinkedList(getLoadedJobs());
		log.info("Adding {} workerdesc, {} triggers.", jobs.size(), triggers.size());
		Iterator<Work> itr = jobs.iterator();
		while (itr.hasNext()) {
			Work detail = itr.next();
			try {
				sched.scheduler(detail, null);
			} catch (Exception e) {
				e.printStackTrace();
			} // add the job if a replacement or durable
		}
		for (Trigger tri : triggers) {
			try {
				sched.getResource().getWorkerStore().storeTrigger(tri, false);
			} catch (ObjectAlreadyExistsException e) {
				e.printStackTrace();
			}
		}
	}

	public void setClassLoadHelper(ClassLoadHelper classLoadHelper) {
		this.classLoadHelper = classLoadHelper;
	}

	public void setLoadedJobs(List<Work> loadedJobs) {
		this.loadedJobs = loadedJobs;
	}
}
